【Linux】分析缓冲区,刷新机制,FILE 您所在的位置:网站首页 Linux Kernel中刷cache函 【Linux】分析缓冲区,刷新机制,FILE

【Linux】分析缓冲区,刷新机制,FILE

2024-06-09 12:31| 来源: 网络整理| 查看: 265

文章目录 一、Linux的缓冲区(一) 用户层缓冲区(二) 内核层缓冲区(Kernel Buffer Cache)验证buffer增加和减少释放缓存 二、缓冲区的刷新策略(一) 用户层缓冲区刷新策略(二) 内核层缓冲区刷新策略 三、探究缓冲区常见问题的产生(一) 由于缺失换行符导致内容没有按预期呈现1、实验设计2、原理分析 (二) 由于提前close(fd)导致内容无法呈现1、实验设计2、原理分析 (三) dup重定向不改变缓冲区刷新方式1、实验设计2、原理分析(1) 用例子阐述原理(2) FILE发生读写时才分配得缓冲区(3) 探究FILE刷新策略何时被指定 (四) fork前没有清空缓冲区 四、总结本文做了以下工作或得出以下结论之后的工作知识补充

一、Linux的缓冲区

在学习中我们会经常遇到两个缓冲区概念,一个是用户层的缓冲区,另一个是内核层的缓冲区。本文主要讨论用户层缓冲区的知识点以及不同的坑

(一) 用户层缓冲区

标准IO库自带缓冲区,像stdin,stdout,stderr这些都是FILE*文件流,FILE*指向一个FILE结构体,结构体包含了缓冲区基地址和末尾地址,还封装了fd

FILE结构体关键代码如下:

struct _IO_FILE { int _flags; char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; /* fd */ };

设置用户层缓冲区的目的和好处:为了减少read,write等系统调用的次数,从而减少用户态和内核态的切换次数,降低系统的开销

(二) 内核层缓冲区(Kernel Buffer Cache)

内核层缓冲区为buffer和cache,它们位于内核空间,被所有进程可见。buffer和cache是内存的不同的体现,它们搭建了CPU和磁盘快速交互的桥梁

buffer存储暂未写入到磁盘的数据,积攒到一定量后写入磁盘,可以降低和磁盘IO的频率cache实现数据预读的功能:可以暂时存储来自磁盘的数据,提高这部分数据重用性,使得OS无需频繁访问磁盘

设置内核缓冲区的好处:内核缓冲区数据不写回磁盘也能被其它进程读取,在这点的作用上和磁盘存储文件无异,直接读取内核缓冲区的数据,带来了读写的高效性

验证buffer增加和减少

增加buffer 对上述命令的解释如下:

读取/dev/zero文件时,它会提供无限的空字符nul,一个常见的用法是产生一个特定大小的空白文件 创建一个1000M的txt文件,其内容为空: dd if=/dev/zero of=test.txt count=10M bs=100 if:输入文件,默认为标准输入 of:输出文件名,默认为标准输出 bs:块大小,同时设置读入/输出的块大小为bytes个字节 count:块个数 释放缓存

在书房缓存前先指向sync讲缓存的数据写到磁盘避免数据丢失,随后输入 echo 3 >/proc/sys/vm/drop_caches释放slab和页缓存 在这里插入图片描述

二、缓冲区的刷新策略 (一) 用户层缓冲区刷新策略

缓冲区有三种类型对应三种刷新缓冲区的方式:

全缓冲 当填满标准I/O缓存后才进行实际I/O操作,如将数据从用户层缓冲区拷贝到内核缓冲区。全缓冲的典型代表是对磁盘文件的读写

行缓冲 当输入和输出中遇到换行符时才执行实际I/O操作,典型代表是标准输入stdin和标准输出stdout

无缓冲 不对数据进行缓冲,直接进行I/O,如标准错误stderr就是无缓冲刷新

缓冲区何时会被刷呢&刷新方法:

调用exit()进程结束时会刷新缓冲区,return会自动调用exit(),注意_exit()不会刷新缓冲区当缓冲区满了也会被刷新出来可通过fflush强制将缓冲流中的数据复制到内核缓冲区中流被关闭时也会被刷出来,如调用fclose函数行缓冲遇见'\n'会被刷新出来 (二) 内核层缓冲区刷新策略

Linux以页作为高速缓存的单位,因此刷新内核缓冲区即对页的管理,操作系统会基于LRU 算法回收文件页和匿名页,当缓冲区内容被修改则变为脏页,其数据在合适的时间将会被写到磁盘中去,以保证高速缓存中的数据和磁盘中的数据是一致的。此外:可以通过sync命令可以将内存中的数据写入到硬盘中

三、探究缓冲区常见问题的产生 (一) 由于缺失换行符导致内容没有按预期呈现 1、实验设计

编写一段代码(见下),预期是先输出“hello world”,再sleep3秒

#include #include int main() { printf("hello world"); sleep(3); return 0; }

运行结果如下: 程序运行先sleep了 随后才打印语句 随后才打印“hello world”

2、原理分析

当我们调用printf函数往显示器打印字符串时,采用的是行刷新模式,printf底层调用stdout这个流文件,当遇到\n时stdout能立马刷新FILE结构体维护的缓冲区。而上面代码并没有携带\n,故hello world这个语句一直停留再FILE维护的缓冲区中,直到最后return 0;语句调用exit函数,exit执行清理缓冲区的操作,hello world才刷新到屏幕,此时已经到程序末尾,故会出现先sleep才打印字符串的现象

(二) 由于提前close(fd)导致内容无法呈现 1、实验设计

编写一段代码(见下),先以写权限打开txt文档,然后利用dup2将1号fd标准输出重定向到txt文档,最后向txt文档写入Hello 1,结束后关闭打开的文件

#include #include #include int main() { FILE *pfd = fopen("text.txt","w"); int fd = fileno(pfd); if (fd FILE *pfd = fopen("text.txt","w"); int fd = fileno(pfd); if (fd char buf[24]; FILE *myfile = stdin; printf("before reading\n"); printf("myfile base %p\n", myfile); printf("read buffer base %p\n", myfile->_IO_read_base); printf("read buffer length %ld\n", myfile->_IO_read_end - myfile->_IO_read_base); printf("write buffer base %p\n", myfile->_IO_write_base); printf("write buffer length %ld\n", myfile->_IO_write_end - myfile->_IO_write_base); printf("buf buffer base %p\n", myfile->_IO_buf_base); printf("buf buffer length %ld\n", myfile->_IO_buf_end - myfile->_IO_buf_base); printf("\n"); fgets(buf, 24, myfile);//read printf("after reading\n"); printf("read buffer base %p\n", myfile->_IO_read_base); printf("read buffer length %ld\n", myfile->_IO_read_end - myfile->_IO_read_base); printf("write buffer base %p\n", myfile->_IO_write_base); printf("write buffer length %ld\n", myfile->_IO_write_end - myfile->_IO_write_base); printf("buf buffer base %p\n", myfile->_IO_buf_base); printf("buf buffer length %ld\n", myfile->_IO_buf_end - myfile->_IO_buf_base); return 0; }

结果如下: 在这里插入图片描述 从实验结果可以看到,还没发生读取时缓冲区地址还没分配,在读入hello后,缓冲区被分配,大小为1024Bytes

(3) 探究FILE刷新策略何时被指定

验证:当FILE结构体获得时其缓冲刷新策略还没有被指定,当FILE发生读写时首次指定刷新策略 首先指出在FILE结构体里的_flags变量的作用相当于位图,它的某些位表示了缓冲区刷新方式

对【 (二) 1、实验设计】中的代码,即还没增加printf("Hello 0\n");的代码进行调试 在这里插入图片描述 可以看到,当代码执行完printf("Hello 1\n");后_flags值改变,具体而言是低8位到低15位从0x20变为0x28。接下来我们深入printf函数看看到底执行了什么导致标志位改变 在这里插入图片描述 当_flags按位与_IO_CURRENTLY_PUTTING后,_flags这个位图某些位发生以下变化

0x20:0010 0000 0x28:0010 1000

综上:【 (二) 1、实验设计】中的代码,重定向后执行printf,stdout采取的是全缓冲刷新,深入调试查看源代码发现,_flags的_IO_CURRENTLY_PUTTING标志位被设置,表示缓冲区内容被设置,但是没有出现对_flags行缓冲标志位的设置

那么要对_flags设置行缓冲应该设置什么标志位呢?接下来对【 (三) 1、实验设计】中的代码,即增加printf("Hello 0\n");的代码进行调试 在这里插入图片描述 可以看到,当代码执行完printf("Hello 0\n");,即stdout首次发生读写后_flags值改变,具体而言是低8位到低15位从0x20变为0x22。继续深入printf函数看看到底执行了什么导致标志位改变 在这里插入图片描述 当_flags按位与_IO_LINE_BUF后,_flags这个位图某些位发生以下变化

0x20:0010 0000 0x22:0010 0010

故而当FILE结构体是执行行缓冲刷新策略时,_flags位图的_IO_LINE_BUF标志位被设置为1 此外,在设置_IO_LINE_BUF标志位后,由于缓冲区有内容了,所以_IO_CURRENTLY_PUTTING标志位也会被设置,故最终执行完printf("Hello 0\n");后_flags低8位到低15位从0x20变为0x2a

0x20:0010 0000 0x2a:0010 1010

此外笔者还对setvbuf函数进行测试,该函数能指定了文件缓冲的模式,函数原型如下:

int setvbuf(FILE *stream, char *buffer, int mode, size_t size)

关于函数的细节可见这个网站的说明:函数用法 经过设置不同的参数,分别进行gdb调试,再结合上述调试成果,最终得到对_flags位图中有关于文件缓冲模式相关的位探究清楚了,结论见下图:

在这里插入图片描述

(四) fork前没有清空缓冲区

当fork前用户层缓冲区仍有数据,在fork后父子进程的缓冲区都保留这些数据,故而当fork后执行输出时,会出现有些内容重复输出了两次。对此建议读者对用户层缓冲区的刷新机制有所了解,当不确定缓冲区内容是否刷新出去时可以调用fflush函数强制刷新

四、总结 本文做了以下工作或得出以下结论

1、系统区分了用户层和内核层缓冲区,指出两者不同之处和特点 (a) 不同之处:两个缓冲区位置不同;用户层缓冲区目的为了减少read,write等系统调用的次数;系统层缓冲区目的为了减少与磁盘IO次数 (b) 相同之处:都是为了提高IO性能,效率

2、 归纳了用户层缓冲区的三种刷新策略/文件缓冲的模式,分别为:行缓冲,全缓冲和无缓冲

3、分析了用户层缓冲区引起的常见问题 (a) 不清楚缓冲区刷新策略是刷新方式导致内容残留 (b) 提前close(fd)导致用户层缓冲区数据无法与内核层缓冲区流动 © fork导致内容重复输出,本质是fork会”复制“用户层数据 (d) dup无法更改FILE刷新机制,FILE在首次读写文件时根据文件类型永久确定刷新机制

4、在源码层面分析了FILE结构体,结构体里有多个指针指向缓冲区维护缓冲区,_flags变量以位图模式解读,总结了刷新策略在_flags位图上的体现 在这里插入图片描述

之后的工作 尝试从系统数据结构角度分析一个进程运行到结束,printf从调用到输出的流程,预想到的知识点有以下: task_struct,内存布局,页表files_struct,fd_array,struct file,inode,文件引用次数,VFS 分析对比联系FILE,struct file,inode,fd之间关系,如何逐层调用 知识补充

零拷贝技术 本文主要讨论了IO,在传统IO中,当有两个fd需要数据流动,如磁盘文件fd和网络文件fd通信,需要先将数据从磁盘拷贝到内核缓冲区,用户再调用系统接口read到用户层,然后再write到内核缓冲区,最后由内核缓冲区将数据刷新到网络文件fd。其过程冗长且拷贝频繁,该数据流动过程见下图橙色线,那么有没有更高效的IO方式呢? 在这里插入图片描述

答案是有的,通过零拷贝技术可以完成上图绿色线的数据流动,即数据只通过内核层就能到达对方fd,零拷贝技术有:sendfile,mmap…

对文章内容的总结图: 在这里插入图片描述

说明:此图部分素材来自网络,侵权删

图解说明:  以进程视角开始,task_struct切入,task_struct里有files_struct结构体,其里面有一个数组fd_array,数组下标即为fd,数组内容是struct file,每一个struct file都对应磁盘一个被打开的文件,OS通过管理内核层的struct file来管理磁盘中被打开的文件。当进程打开一个文件,内核会创建struct file,并在fd_array寻找未被使用的最小下标作为fd,数组值填上struct file*指针,同时用户层/语言层面会创建FILE结构体,封装fd,当文件发生首次读写时,FILE结构体指定刷新方式,开辟缓冲区,通过自身封装的fd找到对应的file struct完成读写几组对应概念: fd和内核层的struct file一一对应 一个fd可以被多个FILE封装,如stdout和stdin封装同一个fd 一个FILE里只有一个fd FILE位于用户空间,内核缓冲区位于内核空间


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有